package com.gjh.ouygoing.service.impl;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.gjh.ouygoing.config.WxPayConfig;
import com.gjh.ouygoing.entity.OrderInfo;
import com.gjh.ouygoing.entity.RefundInfo;
import com.gjh.ouygoing.enums.OrderStatus;
import com.gjh.ouygoing.enums.wxpay.WxApiType;
import com.gjh.ouygoing.enums.wxpay.WxNotifyType;
import com.gjh.ouygoing.enums.wxpay.WxTradeState;
import com.gjh.ouygoing.service.OrderInfoService;
import com.gjh.ouygoing.service.PaymentInfoService;
import com.gjh.ouygoing.service.RefundInfoService;
import com.gjh.ouygoing.service.WxPayService;
import com.gjh.ouygoing.util.GsonUtil;
import com.gjh.ouygoing.util.ValidCodeUtil;
import com.google.gson.Gson;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 实现微信支付的方法
 */
@Service
@Slf4j
public class WxPayServiceImpl extends ValidCodeUtil implements WxPayService {
    /**
     *调用我的这个发送响应的接口
     */
    @Autowired
    @Qualifier("WXPayClient")
    private CloseableHttpClient wxClient;
    /**
     * 调用我的这个微信支付接口的工具类
     */
    @Autowired
    private WxPayConfig wxPayConfig;
    /**
     * 引入我的这个订单的服务层
     */
    @Autowired
    private OrderInfoService orderInfoService;
    /**
     * 引入我的这个微信支付日志的对象
     */
    @Autowired
    private PaymentInfoService paymentInfoService;
    /**
     * 不响应签名结果
     */
    @Autowired
    @Qualifier("wxPayNoSignClient")
    private CloseableHttpClient wxPayNoSignClient; //无需应答签名
    /**
     * 调用我的这个微信的退款单记录
     */
    @Autowired
    private RefundInfoService refundInfoService;
    //定义一个lock来进行一个线程的管控
    private ReentrantLock lock = new ReentrantLock();

    /**
     * 生成二维码接口，同意调用API
     * @param productId 商品编号
     * @return Code_Url 返回我的二维码信息
     */
    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception {
        log.info("生成订单流程开始");
        OrderInfo orderInfo = orderInfoService.creteOrderByProductId(productId);//得到我的这个OrderInfo的对象
        log.info("生成订单流程结束");
        log.info("统一下单");
        if(StringUtils.isNotBlank(orderInfo.getCodeUrl()) && ObjectUtils.isEmpty(orderInfo)) {
            log.info("得到二维码订单已存在");
            //把我的这个二维码的信息响应给前端
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl",orderInfo.getCodeUrl());//返回我的这个二维码相关信息
            map.put("OrderNo",orderInfo.getOrderNo());//得到我的这个订单编号
            return map;//返回我的这个相关信息
        }
        //将订单存储到数据库
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));//用来调用我的这个二维码请求地址
        // 请求body参数
        Gson gson = new Gson();
        Map paramMap = new HashMap();//用来 定义一个集合存储我的这个调用微信支付接口的信息，他的这些参数在官网上有所指定
        paramMap.put("appid",wxPayConfig.getAppid());//用来获得我的这个AppId
        paramMap.put("mchid",wxPayConfig.getMchId());//有来获取我的商户Id
        paramMap.put("description",orderInfo.getTitle());//设置我的这个商户描述
        paramMap.put("out_trade_no",orderInfo.getOrderNo());//设置订单编号
        paramMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));//得到我们的这个微信支付之后的通知地址

        Map<String, Object> amountMap = new HashMap<>();//因为我们在官网当中他的这个订单是单独列出来的,所以我们需要重新定义一个集合存入到我的这个Map中
        amountMap.put("total",orderInfo.getTotalFee());
        amountMap.put("currency","CNY");//设置这个货币类型
        paramMap.put("amount",amountMap);//把我的这个订单的集合存入到我的这个集合当中
        //将参数转换成字符串类型
        String jsonParams = gson.toJson(paramMap);//把我们的这个订单货币信息转换成json格式
        log.info("请求参数"+jsonParams);
        StringEntity entity = new StringEntity(jsonParams,"utf-8");//把我们的这个请求参数放到我们的这个entity的这个对象当中，并且更改我的这个编码
        entity.setContentType("application/json");//内容类型
        httpPost.setEntity(entity);//把这个对象放到这个post请求中
        httpPost.setHeader("Accept", "application/json");
        //完成签名并执行请求，向微信进行远程的请求
            CloseableHttpResponse response = wxClient.execute(httpPost);
            String bodyAsString = ValidCodeCommon(response);
        //响应我当前的这个相关的结果
            Map<String,String> hashMap = gson.fromJson(bodyAsString, HashMap.class);
            //解析二维码
            String codeUrl = hashMap.get("code_url");//从这个HashMap集合当中得到我的这个二维码信息
            orderInfoService.saveCodeUrl(orderInfo.getOrderNo(), codeUrl);//把我的这这个二维码的codeUrl的信息进行更改
            //把我的这个二维码的信息响应给前端
            Map<String, Object> map = new HashMap<>();
            map.put("codeUrl",codeUrl);//返回我的这个二维码相关信息
            map.put("orderNo",orderInfo.getOrderNo());//得到我的这个订单编号
            return map;//返回我的这个相关信息
    }

    /**
     * 校验参数，生成相关订单
     * @param bodyMap 我的这个请求的集合
     */
    @Override
    public void processOrder(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("验证我的这个请求参数");
        //TODO:明文解密
        String plainText = decryptFormatResource(bodyMap);//得到微信端响应的明文数据
        //得到了明文数据了之后需要进行一下步骤
        //TODO:明文转换成Map集合
        //得到我的这个明文数据的Map集合
        HashMap hashMap = GsonUtil.fromJson(plainText);
        //通过我们的这个Map集合得到我们的这个订单编号，然后我们可以通过这个订单编号去更改我的这个支付状态
        // 这些订单状态的key我们可以通过微信支付官网查看
        String orderNo = (String) hashMap.get("out_trade_no");
        //如果我的这个线程得到我的这个锁之后我返回一个true，如果说返回false的话我就会返回false，这个线程不会进行等待
        // 我们在使用lock锁的时候我们需要给他进行锁的释放，我们可以通过使用try catch的形式来进行锁的释放
        // 锁记得加上最大超时时间，不能不设置
        if (lock.tryLock()) {//添加一个锁的机制防止高并发的进行发送请求

            try {
                //TODO:用来解决微信平台重复发送请求
                //用来解决我的这个微信平台在重复的发送请求信息的时候进行显示
                String orderStatus = orderInfoService.getOrderInfoService(orderNo);//更具我的这个订单号拿到我的这个订单转台
                if (OrderStatus.SUCCESS.getType().equals(orderStatus)){//判断我的这个订单是否支付成功，如果支付成功我则直接返回，不往下执行
                    return;
                }
                //TODO:更改这个支付状态
                //更改我的这个支付状态
                orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);//更具我的这个支付订单号更改我的这个订单状态
                //TODO:记录支付日志
                //记录支付日志的时候我们可以把这个支付报文传入到这个创建订单的方法当中
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                lock.unlock();//释放我当前的这个lock锁
            }
        }
    }

    /**`
     * 响应我的 这个明文数据
     * @param bodyMap
     * @return
     */
    private String decryptFormatResource(Map<String, Object> bodyMap) throws GeneralSecurityException {
        log.info("密文解密方法开始");
        //得到这个解密对象，但是这个对象需要传入我的这个微信的APIMV3的这个参数
        //需要将我的这个数据转换成Butes的数组类型的对象
        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //得到我的这个通知数据
        Map<String,String> resourceMap =(Map<String, String>) bodyMap.get("resource");
        //得到我的这个附加数据
        byte[] associatedData = resourceMap.get("associated_data").getBytes(StandardCharsets.UTF_8);
        log.info("密文数据==>{}",associatedData);
        //得到我的这个随机串
        byte[] nonce = resourceMap.get("nonce").getBytes(StandardCharsets.UTF_8);
        //得到这个密文数据
        String ciphertext = resourceMap.get("ciphertext");
        //这个是我的这个揭秘对象
        String plainText = aesUtil.decryptToString(associatedData, nonce, ciphertext);//得到我的这个明文数据，但是他需要去传递一些相关参数
        log.info("明文数据==>{}",plainText);
        log.info("密文解密方法结束");
        return plainText;//返回我的这个明文信息
    }

    /**
     * 用来取消我当前的这个支付订单
     * @param orderNo
     */
    @Override
    public void cancelOrder(String orderNo) throws Exception {
        //在这里我们有两个操作需要执行,第一个操作就是再微信支付端进行关单操作
        //第二个操作就是说更改我的这个数据库中的这个支付状态
        //TODO:调用微信接口实现关单的功能
        this.closeOrder(orderNo);
        //TODO:更改自己的本地数据库实现修改订单状态
        orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CANCEL);
    }

    /**
     *执行我的这个微信远程的关单接口
     * @param orderNo
     */
    private void closeOrder(String orderNo) throws Exception {
        log.info("实现我的这个关单的接口");
        //创建我的这个远程请求的对象
        String url = wxPayConfig.getDomain()
                //WxApiType.CLOSE_ORDER_BY_NO.getType()这个是我们的这个微信支付的状态，他会临时会进行修改
                .concat(String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(),orderNo));//因为我的这个微信支付的这个变量他里面有个微信支付的关单接口，所以我们要使用format和订单编号来进行拼接
        HttpPost httpPost = new HttpPost(url);
        //组装我们的这个请求路径，并且转换成json格式进行发送
            try {
                Gson gson = new Gson();
                Map <String,String> paramsMap = new HashMap<>();//实例化一个hashMap集合
                //微信官网，关单操作：https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml
                //根据我们的这个微信支付官网所说的，需要提供我的这个微信支付的商户号存入我们的这恶鬼Map集合当中
                paramsMap.put("mchid",wxPayConfig.getMchId());
                String jsonParams = gson.toJson(paramsMap);//把我们的这个map集合转换成json格式进行返回
                log.info("发送请求信息==>{}",jsonParams);//将我们的这个请求参数进行日志打印
                //将请求参数这知道我们的这个请求对象当中
                StringEntity entity = new StringEntity(jsonParams,"utf-8");//把我们的这个请求参数放到我们的这个entity的这个对象当中，并且更改我的这个编码
                entity.setContentType("application/json");//内容类型
                httpPost.setEntity(entity);//把这个对象放到这个post请求中
                httpPost.setHeader("Accept", "application/json");
                //完成签名并执行请求，向微信进行远程的请求
                CloseableHttpResponse response = wxClient.execute(httpPost);//发送我的相关的请求信息
                int statusCode = response.getStatusLine().getStatusCode();//得到我的微信响应的支付状态码
                if (statusCode == 200) {
                    log.info("成功200");
                }else if(statusCode == 204){
                    log.info("成功204");
                }else{
                    log.error("对不起响应失败,响应码==>{}",statusCode);
                 throw new IOException("request filed");//如果响应失败则抛出相关异常信息
                }
            } catch (Exception e) {
                e.printStackTrace();
        }
    }

    /**
     * 响应我的这个微信端的支付信息
     * @param orderNo
     * @return
     */
    @Override
    public String queryOrder(String orderNo) throws Exception {
        log.info("接口查询订单编号=====>{}",orderNo);
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(),orderNo);//得到我的这个查询订单的常量和我的这个微信的订单编号
        url=wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());//通过我的这个微信的商户,给我们的这个参数进行拼接，把我们的这个商户编号
        HttpGet httpGet = new HttpGet(url);//因为我是需要获取我的这个请求信息所以需要得到我的这个Http的get请求对象
        httpGet.setHeader("Accept","application/json");//设置我当前的这个请求头信息
        CloseableHttpResponse response = wxClient.execute(httpGet);//得到我的则个微信请求的接口
        //然后通过这个execute的请求参数得到我的这个响应信息
        String bodyString = ValidCodeCommon(response);
        return bodyString;//返回我的这个响应信息
    }

    /**
     * 根据我的这个订单号查询我的这个微信支付的查单接口
     * 核实订单状态
     * @param orderNo
     */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {
        //微信支付的查单接口
        String result = this.queryOrder(orderNo);
        Gson gson = new Gson();//得到我当前这个订单的状态
        HashMap hashMap = gson.fromJson(result, HashMap.class);//得到我的这个支付状态在我的这个微信支付端是什么状态
        Object tread_state = hashMap.get("tread_state");//得到我的这个Map集合当中的状态信息
        if (WxTradeState.SUCCESS.getType().equals(tread_state)) {//判断我当前的这个微信端的请求状态是否和我的这个本地的状态符合一致，如果 不一致则返回错误信息
            log.info("核实订单已经支付成功=={}",orderNo);
            //如果说我本地的订单状态如果说已经更新成功那么我们就需要去更改我们本地的这个订单状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//用来调用我们这个修改订单状态的方法
            //记录我的这个支付日志
            paymentInfoService.createPaymentInfo(result);//因为我的这个微信支付平台上面所响应的字段是和我的这个数据库上面的字段是一样的，所以可以直接进行赋值
        }
        //如果我的这个微信支付端如果是未支付状态我们就需要去调用我们的这个关单接口
        if (WxTradeState.NOTPAY.getType().equals(tread_state)){
          log.info("订单未支付===>{}",orderNo);
          //调用我的这个微信端的这个远程的这个关单接口
            this.closeOrder(orderNo);//执行我的这个远程订单关单的方法
          //更改我的这个数据库当中的这个订单状态，显示我的这个订单是超时关单的状态
            orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);
        }
    }

    /**
     * 申请退款的方法
     * @param orderNo
     * @param reason
     */
    @Override
    public void refund(String orderNo, String reason) throws IOException {
        //申请订单在这里一共有两个步骤
        // 第一个步骤修改数据库的订单状态，添加退款信息到退款表中
        // 第二申请微信端发送请求进行退款操作
        log.info("更改数据库当中的信息");
        //TODO:修改数据库的退款表
        RefundInfo refundInfo = refundInfoService.createOrefundByOrderNo(orderNo, reason);
        //TODO:向微信端发送请求信息
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());//得到我的这个申请退款的url地址,主机的地址拼接我们的这个退款的地址
        Gson gson = new Gson();//得到我的这个转换成json字符串的订单信息
        HttpPost httpPost = new HttpPost(url);//得到我的这个http请求，向微信服务器发送相关的信息
        Map<String, Object>  paraMap = new HashMap<>();//实例化一个集合用来存储我的这个申请退款的订单信息

        paraMap.put("out_trade_no",orderNo);//存入我的这个订单编号
        paraMap.put("out_refund_no",refundInfo.getRefundNo());//商户退款单号
        paraMap.put("reason",reason);//退款原因
        paraMap.put("notify_url",wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));//设置我的这个订单的退款之后回调的url地址

        Map<String,Object> amount = new HashMap();//定义我的这个退款金额的集合
        amount.put("refund",refundInfo.getRefund());//退款金额
        amount.put("total",refundInfo.getTotalFee());//设置我的这个原单金额
        amount.put("currency","CNY");//设置我的这个货币的类型
        paraMap.put("amount",amount);//把金额的集合存入到这个订单集合中
        String jsonParam = gson.toJson(paraMap);//得到我的这个json字符串
        log.info("请求参数===>{}",jsonParam);
        StringEntity entity = new StringEntity(jsonParam, "utf-8");//通过我的这个json字符串转换成Entity格式的信息
        entity.setContentType("application/json");//设置我的这个-报文的类型  注意：我的这个地址 一定要是contentType
        httpPost.setEntity(entity);//把我的这个entity进行返回
        httpPost.setHeader("Accept","application/json");//设置我当前的这个请求头信息
        CloseableHttpResponse response = wxClient.execute(httpPost);//把这个请求信息存放到这个微信响应的这个里面,最终完成签名
        try {

            String bodyAsString = EntityUtils.toString(response.getEntity());//得到我的这个请求返回参数信息没在出现错误的时候我们需要打印相关日志信息
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                 log.info("成功！！！响应的结果是200");
            }else if (statusCode == 204) {
                 log.info("成功!!！，响应的结果是204");
            }else {
                 throw new Exception("退款异常，响应码是:" + statusCode + "响应的信息是：" + bodyAsString);
            }
            //我们向平台发送了申请退款的请求，那么同时也要进行做一定的操作
            //TODO:更新我们的退款单
            orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.REFUND_PROCESSING);//把我当前的这个转台更改这个信息
            //TODO:更新订单状态
            refundInfoService.updateRefund(bodyAsString);
        } catch (Exception e) {
                e.printStackTrace();
        }
    }

    /**
     * 根据订单编号查询相关的退款信息
     * @param refundNo 订单编号
     * @return
     */
    @Override
    public String  queryRefund(String refundNo) throws IOException {
        log.info("查询退款信息，退款编号==>{}",refundNo);
        //首先第一步我们需要拼接我们的这个请求的url地址
        String url = String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType());//得到我的这个请求的url地址
        url = wxPayConfig.getDomain().concat(url);//把发送请求的前头和后尾的语句进行拼接形成url地址
        HttpGet httpGet = new HttpGet(url);//得到我的这个http的get请求对象并且把这个url地址拼接进去
        httpGet.addHeader("Accept","application/json");
        CloseableHttpResponse response = wxClient.execute(httpGet);//得到我的这个响应
        try {
            String bodyAsString = ValidCodeCommon(response);
            return bodyAsString;
        } catch (Exception e) {
            //异常被捕获，应该需要记录日志
            log.error("");
            e.printStackTrace();

        }finally {
            response.close();//释放当前只有
        }
        return null;
    }

    /**
     * 用来实现操作退款单的信息
     * @param bodyMap
     */
    @Transactional(rollbackFor = Exception.class)//用来开启我的这个事务回滚操作
    @Override
    public void reprocessRefund(HashMap bodyMap) throws Exception {
        bodyMap.get("退款单");
        String plainText = decryptFormatResource(bodyMap);//因为我们的这个微信响应的报文是加密形式的，解密我当前的这个报文信息
        Gson gson = new Gson();//得到解析Json字符串的对象
        HashMap hashMap = gson.fromJson(plainText, HashMap.class);//把这个字符串解析成一个Map集合
        String orderNo = (String) hashMap.get("out_trade_no");//得到我的这个商户号的信息

        String orderStatus = orderInfoService.getOrderStatus(orderNo);//得到我的这个订单状态
        if (OrderStatus.REFUND_PROCESSING.equals(orderStatus)) {//判断我当前的这个订单是否是在退款中的这个状态，如果是退款中的状态则直接返回空
            return;
        }
        orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_PROCESSING);//更改当前的这个退款状态
        refundInfoService.updateRefund(plainText);//把这个报文信息更新到退款申请的数据库当中
    }

    /**
     * 下载我当前的这个交易账单或者是资金账单
     * @param billDate 查询日期
     * @param type 交易账单类型
     * @return
     */
    @Override
    public String queryBill(String billDate, String type) throws Exception {
        log.warn("申请账单下载接口==>{}",billDate);
        String url="";//定义这个url地址,url的组成部分是微信服务器地址+账单下载地址+账单交易类型+请求
        //前端传过来的参数不可信，应该保存账单的数字字典（交易账单）还是资金账单
        if (type.equals("tradebill")) {//判断当前的这个账单是否是交易账单
            url=wxPayConfig.getDomain().concat(WxApiType.TRADE_BILLS.getType());//得到我的这个单笔交易账单的url
        }else if(type.equals("fundflowbill")){//判断当前的这个账单是否是资金账单
            url =wxPayConfig.getDomain().concat(WxApiType.FUND_FLOW_BILLS.getType());//拼接资金账单的信息
        }
        url=url.concat("?bill_date=").concat(billDate);//因为在微信支付官网中显示必须要传递当前的这个查询日期
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept","application/json");//更改我的这个响应头信息
        CloseableHttpResponse response = wxClient.execute(httpGet);//得到后端响应的信息
        try {
            String bodyAsString = ValidCodeCommon(response);
            Map<String,String> hashMap = GsonUtil.fromJson(bodyAsString);
            return hashMap.get("download_url");//得到我的这个excel下载的地址，并返回
        } finally {
            response.close();//释放资源
        }
    }

    /**
     * 下载excel表格的接口方法
     * @param billDate 我的这个查询的时间
     * @param type 订单的类型
     * @return
     */
    @Override
    public String downloadBill(String billDate, String type) throws Exception {
        if(!StringUtils.isEmpty(billDate) && !StringUtils.isEmpty(type)) {
            //抛出自定义异常
        }
        log.info("下载账单的接口调用，时间==>{}，订单类型==>{}",billDate,type);
        String downloadUrl = this.queryBill(billDate, type);//调用到我的这个账单下载url地址的方法
        return download(downloadUrl);
    }
    
    private String download(String downloadUrl) throws Exception {
        HttpGet httpGet = new HttpGet(downloadUrl);//把我的这个申请得到的这个Url地址存放到HttpGet中，通过这个地址去得到我下载Excel表格需要的数据
        httpGet.addHeader("Accept", "application/json");//设置响应头
        log.info("excel下载的网址====>"+downloadUrl);
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);//得到这个响应信息
        try {
            String code = ValidCodeCommon(response);
            //如果响应成功则返回我的这个微信端响应的信息
            return code;
        } finally {
            response.close();//释放资源
        }
    }


    @Override
    public String ValidCodeCommon(HttpResponse response) throws Exception {
        return super.ValidCodeCommon(response);
    }
}
